import pandas as pd
from plotly.offline import download_plotlyjs, init_notebook_mode, plot, iplot
import plotly
import plotly.graph_objs as go
from datetime import date, timedelta
import plotly.express as px
import numpy as np
df = pd.read_csv('../data/report_task_1.csv')
df.head()
df['DateTime'] = df['Hour'].astype('datetime64[ns]')
df['Hour'] = df['DateTime'].dt.hour
df['Date'] = df['DateTime'].dt.date
df.sort_values(['DC', 'Date', 'Hour'], inplace=True)
Исходя из описания тестового задания мы можем сделать следующие выводы:
Инцидент А -- это отсутствие данных в колонках Bids, Impressions и Spent (если мы не отправляем запросы на пратнера, то он на нас не бидит, следовательно у нас нет показов, и заработанных денег)
Инцидент Б -- это отсутствие данных в колонках Impressions и Spent (партнер делает биды, но мы не учитываем показы)
P.S. В случае Инцидент Б необходимо понимать кто является источником данных для колонки Impressions. Если это сервис аналитики, то предположение выше верно. Если клиенты, то у нас будет другая картина (возможно, в этом случае мы не учитываем не только показ, но и ставку, и в таком случае у нас будет низкое количество ставок и большое показов)
Проверим это предположение:
# Инцидент А
df[df['Bids'].isna()].head()
# Инцидент Б
df[df['Impressions'].isna()].head()
Действительно, у нас полностью подтверждается Инцидент Б, но есть проблема с Инцидентом А:
Мы видим наличие показов и прибыли, хотя не должны.
Для понимания такого поведения, нужна дополнительная консультация с бизнесом/разработчиками. Возможно, у нас есть другая проблема, о которой мы не подозреваем (например, наш сервис анатилики не записывает биды).
В рамках тестового задания, упраздним этот процесс и удалим данные в колонках Impressions и Spent, в тех случаях, когда остутствует значение в колонке Bids:
df.loc[df['Bids'].isna(), ['Impressions', 'Spent']] = np.nan
Посмотрим на то, как ведут себя соотношения показов к бидам, и сколько денег приносит нам 1 бид и 1 показ:
df['Spent_diff_Bids'] = df['Spent'] / df['Bids']
df['Spent_diff_Impressions'] = df['Spent'] / df['Impressions']
df['Impressions_diff_Bids'] = df['Impressions'] / df['Bids']
# графики интерактивные, можно выделять определенные диапазоны и смотреть их детальнее
for metrics in ['Spent_diff_Bids', 'Spent_diff_Impressions','Impressions_diff_Bids']:
fig = px.line(df, x='DateTime', y=metrics, color='DC', title=f'{metrics}' )
fig.show()
Как мы видим, у нас действительно присутствуют случаи, когда соотношения показов к ставкам выше 1. Выделим их в Инцидент В
В таком случае, востановим данные не только для пропущенных значений, но и для тех, когда Impresson/Bids > 1 (в дальнейшем сюда можно будет добавить и другие аномальные значения. Например те, которые значительно выше среднего за предыдущие несколько дней)
Для востановленния данных воспользуемся следующим подходом: для каждого дата центра, в рамках кажодого часа просчитаем скользящее оконное среднее значение за последние 5 дней, но не менее 3-ох.
for i in ['A', 'B', 'C']:
df[f'Incident_{i}'] = False
df.loc[df['Bids'].isna(), 'Incident_A'] = True
df.loc[(df['Incident_A']==False) & (df['Impressions'].isna()), 'Incident_B'] = True
df.loc[df['Impressions_diff_Bids']>1, 'Incident_C'] = True
dataframe = list()
metrics = ['Bids', 'Impressions_diff_Bids', 'Spent_diff_Bids', 'Spent_diff_Impressions']
metrics_rename = {i:f'{i}_rolling' for i in metrics}
for dc in ['US', 'EU']:
for hour in range(0,24):
temp = df[(df['Hour'] == hour)&(df['DC'] == dc)][metrics + ['Date']].reset_index(drop=True).copy()
temp[metrics] = temp[metrics].rolling(5, min_periods=3, win_type='gaussian').mean(std=3)
temp['Hour'] = hour
temp['DC'] = dc
temp.rename(columns=metrics_rename, inplace=True)
dataframe.append(temp.fillna(0))
temp = pd.concat(dataframe)
filled_dataset = df.copy()
filled_dataset = pd.merge(left=filled_dataset, right=temp, how='left', on=['Hour', 'DC', 'Date'])
Востановим данные для прощуенных значений
for m in metrics:
fd_na = filled_dataset[m].isna()
filled_dataset.loc[fd_na, m] = filled_dataset.loc[fd_na, f'{m}_rolling']
filled_dataset.loc[fd_na,
'Impressions'] = filled_dataset.loc[fd_na,
'Bids'] * filled_dataset.loc[fd_na,
'Impressions_diff_Bids']
filled_dataset.loc[fd_na,
'Spent'] = filled_dataset.loc[fd_na,
'Bids'] * filled_dataset.loc[fd_na,
'Spent_diff_Bids']
for c in ['Spent', 'Impressions', 'Bids']:
filled_dataset[c] = filled_dataset[c].astype('int32')
Наглядно посмотрим на то, как заполнились данные в один из период, где они отсустуствовали:
describe = filled_dataset[(filled_dataset['Date']>=date(2020, 3, 25)) & (filled_dataset['Date']<=date(2020, 4, 2))
&(filled_dataset['DC']=='US')]
for m in ['Spent_diff_Impressions']:
fig = px.line(describe, x='DateTime', y=m, title=f'{m}' )
fig.show()
describe = df[(df['Date']>=date(2020, 3, 25)) & (df['Date']<=date(2020, 4, 2))
&(df['DC']=='US')]
for m in ['Spent_diff_Impressions']:
fig = px.line(describe, x='DateTime', y=m, title=f'{m}' )
fig.show()
def calculate_metrics(df_t, name):
for dc in ['US', 'EU']:
temp = df_t[df_t['DC']==dc].copy()
temp['Delta_between_incident'] = temp['DateTime'] - temp['DateTime'].shift(1)
meen_days_between = temp[temp['Delta_between_incident']>timedelta(hours=1)]['Delta_between_incident'].mean()
incident_count = temp[temp['Delta_between_incident']>timedelta(hours=1)]['Delta_between_incident'].count()
incident_hour = temp[name].sum()
incident_spent = int(temp['Spent'].sum())
print(f'''DC: {dc}
Incident hours: {incident_hour}
Incident spent: {incident_spent}
Days incident between: {meen_days_between}
Incident count: {incident_count + 1}
Mean hours: {incident_hour/(incident_count + 1)}''')
Подсчитаем количество часов и потерь от Инцидента А
incident_a = filled_dataset[filled_dataset[f'Incident_A']==True]
calculate_metrics(incident_a, 'Incident_A')
Подсчитаем количество часов и потерь от Инцидента Б
incident_b = filled_dataset[filled_dataset[f'Incident_B']==True].copy()
calculate_metrics(incident_b, 'Incident_B')
Подсчитаем количество часов и потерь от Инцидента В
Продалжая логику, описанную выше, мы считаем:
что не все ставки и доход от них был записан, а прибыль(Spent) и показы -- это то, что мы получили от клиентов.
Следовательно, наши потери:
(прибыль) - (востановленная прибыль от ставки) * (количество бидов которое мы посчитали)
incident_c = filled_dataset[filled_dataset[f'Incident_C']==True].copy()
incident_c['Spent'] = incident_c['Spent'] - incident_c['Bids'] * incident_c['Spent_diff_Bids_rolling']
calculate_metrics(incident_c, 'Incident_C')